哈囉~大家,恭喜來到第一週的最後一天 Day 7 ! 🙌 🙌 🙌
在前面幾天學完 Go 的基本語法練習之後,這個章節就要來介紹 Go 的單元測試!
什麼是單元測試呢?
我們以齒輪為例。每一個齒輪就像是一個單元,而齒輪跟齒輪之間又可以組裝成一個比較大的齒輪(大單元),以此類推… 最後完成一個機械設備。
而當我們要量產這些機械設備的時候,就會需要好多齒輪來組裝。但又不希望這些組裝完成的設備品質參差不齊,所以這時候單個齒輪的品質就很重要!
每個小齒輪製作完成後,需要經過一系列的檢驗,看看有沒有符合完美齒輪的標準。
👉 而這就是「 單元測試 」!
單元測試的概念就是測試程式最小功能 function。讓我們的程式能夠在越長越大的時候,依然保有一定的品質。
理解完大致概念之後,就要來進入 Go 的單元測試的介紹了~
先來介紹一些命名的規則以及需要特別注意的地方:
_test.go
結尾。 (例如:calc.go
→ 測試檔命名為 calc_test.go
)Test
開頭。t *testing.T
。t.Error
或 t.Fatalf
方法印出錯誤訊息。
t.Errorf
→ 報錯但繼續跑其他測試。t.Fatalf
→ 報錯並立刻中止測試。我們拿前面有提到的相加函式來舉例。以下是主要程式以及測試檔案內容:
// 要測試的程式 calc.go
package calc
func Add(a, b int) int{
return a + b
}
// 測試檔案 calc_test.go
package calc
import "testing"
func TestAdd(t *testing.T) {
result := Add(2, 3)
if result != 5 {
t.Errorf("Add(2,3) 預期=5,得到=%d", result) // 印出錯誤訊息
}
}
執行時,要記得先到這兩個檔案的路徑底下輸入指令才能順利跑測試喔!
以下這兩個指令都可以~ 差別在於顯示的內容詳細程度。
// 輸入測試指令
go test
go test -v
分別輸出:
go test
:
go test -v
:
那如果是測試失敗的話會長什麼樣子呢? 就會看到有印出 FAIL 的字樣!
測試失敗輸出:
這是最基礎的測試撰寫和操作方式~
再來我們來看看,可不可以新增多的測試資料? 答案是:當然可以!
這時候就要結合我們前面學到的 array 還有 struct 的功能啦~
這邊除了介紹測試多個測資,我們再多放入子測試的概念,畢竟這兩個很常會一起使用~所以我們就一起介紹!
什麼叫做 子測試(Subtests)? 就是在一個測試 function 裡面還有一個測試,那裡面的測試就叫做子測試。有點像一個傳入一個的概念,我們把主要的測試內容寫在一開始的測試 function,執行測試時,這些測試資料就會傳入子測試!
會有子測試的原因就是,當你需要記錄測試名稱時,你可以使用這樣的方式~
子測試的建立方式:
t.Run()
方式。name
:測試資料名稱。f
:子測試的 function,接收一開始定義好的測資。// 子測試建立方式
t.Run(name string, f func(t *testing.T))
來看看範例:
// 新增多個測資的測試程式 calc_test.go
package calc
import "testing"
func TestAdd(t *testing.T) {
tests := []struct {
name string // 新增測試名稱,不新增也是可以的
a, b int
sum int
}{
{"test 1", 3, 2, 5}, // 測試資料
{"test 2", 5, 0, 5},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { // 這邊是子測試(Subtests)
got := Add(tt.a, tt.b)
if got != tt.sum {
t.Errorf("got %d, sum %d", got, tt.sum)
}
})
}
}
輸出:
測試名稱也會同時印出來,方便我們 debug 用~
有單個測試、多個測試當然也有平行測試!
什麼叫做「平行測試」呢?
我們都知道程式執行時,是一行一行接著跑,而平行測試就是可以同時執行的意思!言下之意,表示可以加速整個測試的過程,減少測試時間。
但它只能使用在「互相不影響的測試程式中」!!!
Go 提供 t.Parallel()
方法,讓測試程式可以同時執行。延續上面的程式,要怎麼加入平行測試呢?
範例:
// 新增平行測試的測試程式 calc_test.go
package calc
import (
"testing"
"time"
)
func TestAddParallel(t *testing.T) {
tests := []struct {
name string
a, b int
sum int
}{
{"test 1", 3, 2, 5},
{"test 2", 5, 0, 5},
}
for _, tt := range tests {
tt := tt // 避免抓到同一個變數!!
t.Run(tt.name, func(t *testing.T) {
t.Parallel() // 讓子測試平行執行,也就是 test 1, 2 會一起執行
time.Sleep(1 * time.Second) // 增加一些運算時間,避免看不出差異
got := Add(tt.a, tt.b)
if got != tt.sum {
t.Errorf("got %d, want %d", got, tt.sum)
}
})
}
}
輸出:(有使用 t.Parallel()
)
那我們來看看,如果沒有使用 t.Parallel()
,會需要多少時間?
先把這兩行程式註解,然後再跑一次測試~
// 註解以下程式
// tt := tt
// t.Parallel()
輸出(沒有使用 t.Parallel()
):
結論是,沒有使用平行測試需要大概 3 秒;使用平行測試則只要 2 秒。雖然這個範例減少的時間沒有很有感覺 😆 但如果使用在比較複雜的結構上,是真的會少很多時間!
不過還是要特別注記一下:
如果遇到想要跳過的測試項目,我們可以用 t.Skip(“這裡放跳過原因”)
跳過,只驗證程式邏輯就好。
Go 提供 testing.Short()
方法,標記需要跳過測試的程式,而當我們輸入 go test -short
執行測試時,testing.Short()
會回傳 true
,然後一樣跑完測試會有輸出內容。在輸出內容中就可以看到被我們標記跳過的部分。
範例:
// 新增跳過標記的測試程式 calc_test.go
package calc
import "testing"
func TestAdd(t *testing.T) {
tests := []struct {
name string
a, b int
sum int
}{
{"test 1", 3, 2, 5},
{"test 2", 5, 0, 5},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := Add(tt.a, tt.b)
if testing.Short() { // 標記跳過測試
t.Skip("跳過測試,因為跑 -short")
}
if got != tt.sum { // 所以這一段不會被執行
t.Errorf("got %d, sum %d", got, tt.sum)
}
})
}
}
輸出:
從輸出的結果中會發現,測試程式並沒有執行「標記 testing.Short()
」的後面程式。且輸出的結果顯示的並不是失敗,而是寫上了 SKIP!然後順利跑完整個測試。
再來,還有一個比較特別的測試方式,就是 「使用註解的方式來表示正確的結果」 讓測試程式來比對!這個就稱為 Example 測試。
測試的命名要使用 Example 開頭,例如:ExampleAdd
。而測試的方式就是看你輸入的結果是否等於註解的結果。是不是很特別~
範例:
// Example test
package calc
import "fmt"
func ExampleAdd() {
fmt.Println(Add(2, 3))
// Output: 5 // 這邊是放解答!
}
輸出:
👉 比對是否符合 // Output:
的結果。
最後!除了驗證程式的邏輯是否正確之外,程式的效能也是很重要的~
而 Go 非常貼心,它也內建了效能檢測的方式,叫做 Benchmark 測試。一樣寫在 _test.go
的檔案裡面就可以了唷!
// 效能測試 **Benchmark**
package calc
import "testing"
func BenchmarkAdd(b *testing.B) {
for i := 0; i < b.N; i++ {
Add(1, 2)
}
}
執行時,輸入:
go test -bench=.
就會得到輸出:
Go 就會自己幫你計算運行這一個函式所用到的資源和結果,以下是輸出說明:
BenchmarkAdd-11
:測試函式名稱。11
:同時運行的 Goroutine 數量(由 Go 測試框架自動決定,和 CPU 核心數相關)。1000000000
:執行次數。0.3831 ns/op
:平均每次呼叫的時間,單位「奈秒」。以上就是 Go 的單元測試介紹了~
內容雖然有一點多,但看完會發現其實 Go 的測試結構不算複雜,只要把所有的測試 function 放進 _test.go
的檔案裡面就可以了~